/**
 * diff组件，用于生成带评论的diff
 */

import React from 'react';
import PropTypes from 'prop-types';
import { parseDiff, Diff, Hunk, getChangeKey, markWordEdits } from 'components/diffView';
import { Balloon, Loading } from '@teamix/ui';
import { YunxiaoIcon } from '@alife/devops-ui';
import remove from 'lodash.remove';
import sha1 from 'sha1';
import RuleCard from 'components/ruleCard/rule';
import hljs from 'utils/hl.js';
import 'components/diff/index.scss'; // diff的公共样式
import 'components/diff/atom-one-light.scss';
import ListLoading from 'atComponents/ListLoading';
import P3cDetailDialog from './p3cDetailDialog';
import InlineIssueDetailDialog from './inlineIssueDetailDialog';
import Conversation from './conversation';
import track from 'utils/track';
import utils from 'utils';
import { getPathWithRootPath } from 'utils/router';
import IssueCard from './issueCard';
import ListEmpty from 'atComponents/ListEmpty';
import './index.scss'; // 带评论diff自己的样式
import intl from '../../../../../../../../../../../locale';

const { pathResource } = window.FORCE;

export default class DiffWithComment extends React.Component {
  static propTypes = {
    diffText: PropTypes.string,
  };

  static defaultProps = {
    diffText: 'overview',
  };

  static restConversationsOfThisDiff = [];

  state = {
    restConversationsOfThisDiff: [],
  };

  // 定时检测是否有讨论ID
  timer = null;

  componentDidMount() {
    if (this.props.forceCrFileViewType === 'single' && this.props.hash.conversationId) {
      this.timer = setInterval(() => {
        const conversationDom = document.getElementById(
          `anchor_conversation_${this.props.hash.conversationId}`,
        );
        const mrBody = document.querySelector('#mr-content-body');
        if (conversationDom) {
          clearInterval(this.timer);
          // 初始化时距离计算
          mrBody.scrollTop = conversationDom.getBoundingClientRect().top;
        }
      }, 50);
    }
  }

  componentWillUnmount() {
    clearInterval(this.timer);
  }

  // 拼接line_code
  static getLineId = (change, oldPath, newPath, diff) => {
    // 如果文件被删除，则newPath都为/dev/null;所以取oldPath
    if (diff.deleted_file) {
      return `${sha1(oldPath)}_${change.oldLineNumber}_${change.newLineNumber}`;
    }
    return `${sha1(newPath)}_${change.oldLineNumber}_${change.newLineNumber}`;
  };

  componentDidUpdate(prevProps) {
    // jump_line_code 状态不为空，即认为有未消费的待跳转行号
    // 这时需判断有无这行的dom（因为不知啥时候diff组件能渲染完），有的话进行跳转，并清空 jump_line_code
    const mrBody = document.querySelector('#mr-content-body');
    if (this.props.jump_line_code) {
      const { file, side, line } = this.props.jump_line_code;
      const lineAnchorDom = document.getElementById(`anchor_line_${file}_${side}_${line}`);

      if (lineAnchorDom) {
        // 先滚到最上面
        mrBody.scrollTop = 0;

        // const hasAoneNoticeBar = document.querySelector('.has-aonel-notice');
        // const aoneNavHeight = hasAoneNoticeBar ? 70 : 45;

        // 如果未展示过引导，则不用滚，否则引导样式会乱
        // if (!this.props.needIntro) {
        const parentOffsetHeight = lineAnchorDom.offsetParent
          ? lineAnchorDom.offsetParent.offsetTop
          : 0;
        mrBody.scrollTop = lineAnchorDom.offsetTop + parentOffsetHeight;
        // }

        this.props.updateJumpLineCode(null);
        this.props.updateJumpFile(null);
      }
      return;
    }

    // 这种情况是切换文件，需要把文件头定位到最上面
    if (this.props.jump_file) {
      if (
        this.props.jump_file === this.props.hash.file ||
        this.props.jump_file === sha1(this.props.diffs[0].new_path)
      ) {
        // mrBody.scrollTop = 0;
        const diffComponents = document.querySelectorAll('.diff');
        const fileListDom = document.getElementById('J_file_List');
        // 一个 .diff 出现，代表一个diff渲染完了
        if (!fileListDom) {
          this.props.updateJumpFile(null);
          return false;
        }
        if (this.props.forceCrFileViewType === 'single') {
          this.props.updateJumpFile(null);
          return false;
        } else {
          // 需要渲染的diff都渲染完才去滚动 (排除文件过大只渲染提示的$WAITING_FOR_CLICK_BIG_SIZE$)
          const needRenderDiffs = this.props.diffs.filter((item) => {
            return item.showDiff && item.diff !== '$WAITING_FOR_CLICK_BIG_SIZE$';
          });

          if (needRenderDiffs.length === diffComponents.length) {
            const fileDom = document.getElementById(`auchor_file_${this.props.jump_file}`);
            mrBody.scrollTop = fileDom ? fileDom.offsetTop : 0;
            this.props.updateJumpFile(null);
            setTimeout(() => {
              this.props.updateData({
                isScrollByTree: false,
              });
            }, 50);
          }
        }
      }
    }

    if (
      this.props.hash.file &&
      this.props.isShowComment !== prevProps.isShowComment &&
      this.props.forceCrFileViewType === 'all'
    ) {
      const fileDom = document.getElementById(`auchor_file_${this.props.hash.file}`);
      mrBody.scrollTop = fileDom ? fileDom.offsetTop : 0;
    }
  }

  getCustomEvents = (hunks, oldPath, newPath) => ({
    code: {
      // onDoubleClick: this.addComment,
      onClick: (change, e) => {
        // 点击的是icon，添加评论框
        if (e.target.classList.contains('diff-code-comment-icon')) {
          this.addComment(change, hunks, oldPath, newPath);
        }
      },
    },

    gutter: {
      onClick: (change) => this.addComment(change, hunks, oldPath, newPath),
    },
  });

  getTree = (comments) => {
    // 递归形成讨论块：以无parent_note_id的评论作为讨论块的跟节点，其子评论平铺在下面
    const conversations = [];
    comments.forEach((comment) => {
      const isRoot = !comment.parent_note_id;
      if (isRoot) {
        conversations.push([comment]);
      } else {
        conversations.forEach((conversation) => {
          conversation.forEach((childItem) => {
            if (childItem.id === comment.parent_note_id) {
              conversation.push(comment);
            }
          });
        });
      }
    });
    return conversations;
  };

  getWidgets = (hunks, oldPath, newPath) => {
    const {
      drafts,
      inline_comments,
      writingChanges,
      inline_issues,
      commentsVisibleStatus,
      issueDetailList,
      mergeRequestBuilds,
      diff,
    } = this.props;

    const allChanges = hunks.reduce((result, { changes }) => [...result, ...changes], []);
    // // 下面是为了将diff没返回，但是有草稿和评论的行的数据显示在最底部
    const rightChanges = allChanges.filter((change) => {
      return change.type !== 'delete';
    });
    // 匹配到的行
    const matchedLines = rightChanges.map((item) => {
      return item.newLineNumber;
    });
    // 未匹配到的数据
    const noMatchedComments = inline_comments.filter((item) => {
      return (
        item.side === 'right' && item.path === newPath && matchedLines.indexOf(item.line) === -1
      );
    });
    const noMatchedDrafts = drafts.filter((item) => {
      return (
        item.side === 'right' && item.path === newPath && matchedLines.indexOf(item.line) === -1
      );
    });
    this.state.restConversationsOfThisDiff = this.getTree([
      ...noMatchedComments,
      ...noMatchedDrafts,
    ]);

    return allChanges.reduce((widgets, change) => {
      const changeKey = getChangeKey(change);
      // 根据类型决定传递的line
      const line = change.type === 'delete' ? change.oldLineNumber : change.newLineNumber;

      // 忽略空格时，只匹配 newLineNumber，不然在非忽略空格时的评论可能匹配不到
      const inline_comments_thisLine = inline_comments.filter((item) => {
        // 文件删除根据旧文件名过滤
        if (change.type === 'delete') {
          return item.path === oldPath && item.line === line && item.side === 'left';
        } else {
          return item.path === newPath && item.line === line && item.side === 'right';
        }
      });

      const drafts_thisLine = drafts.filter((item) => {
        // 文件删除根据旧文件名过滤
        if (change.type === 'delete') {
          return item.path === oldPath && item.line === line && item.side === 'left';
        } else {
          return item.path === newPath && item.line === line && item.side === 'right';
        }
      });

      if (this.restConversationsOfThisDiff) {
        remove(this.restConversationsOfThisDiff, (conversation) => conversation[0].line === line);
      }

      const comments = [
        ...inline_comments_thisLine,
        ...drafts_thisLine.map((item) => {
          item.is_draft = true;
          return item;
        }),
      ];

      // 递归形成讨论块：以无parent_note_id的评论作为讨论块的跟节点，其子评论平铺在下面
      // 云豆回复含有issue，不在此处处理。
      const conversations = this.getTree(
        comments.filter((item) => {
          return !item.issue_id;
        }),
      );

      // 匹配issues
      const codeScanResult = [];
      issueDetailList.forEach((item) => {
        if (
          item.file_name === newPath &&
          item.begin_line === change.newLineNumber &&
          change.type !== 'delete'
        ) {
          codeScanResult.push(item);
        }
      });

      const matched_inline_issues = inline_issues
        ? inline_issues.filter((item) => {
            let isLineMatched;
            const { newLineNumber } = change;
            // line_range 可能有 a-b、a,b,c、a 三种情况
            // 在最后一行匹配行展示issue卡片
            if (item.line_range.split('-').length > 1) {
              const end = item.line_range.split('-')[1];
              isLineMatched = Number(end) === newLineNumber;
            } else if (item.line_range.split(',').length > 1) {
              const lineArr = item.line_range.split(',');
              isLineMatched = Number(lineArr[lineArr.length - 1]) === newLineNumber;
            } else if (Number(item.line_range) === newLineNumber) {
              isLineMatched = true;
            }

            return item.file_path === newPath && isLineMatched;
          })
        : [];

      // commentsVisibleStatus 可取3个值：all none unClosed
      // none 为隐藏所有评论
      const isCommentUnVisible = commentsVisibleStatus === 'none';

      // 是否展示行内评论区的判断条件
      const isCommentBlockShow =
        conversations.length ||
        (writingChanges
          ? writingChanges.includes(DiffWithComment.getLineId(change, oldPath, newPath, diff))
          : false);

      if (
        isCommentUnVisible ||
        (!isCommentBlockShow && !codeScanResult.length && !matched_inline_issues.length)
      ) {
        return {
          ...widgets,
        };
      }
      //

      return {
        ...widgets,
        [changeKey]: (
          <div className="diff-inline-container">
            {codeScanResult.length ? (
              <div
                className="inline-rule-card-container"
                id={`violation-${newPath}-${change.newLineNumber}`}
              >
                {codeScanResult.map((item) => {
                  let author = {};
                  mergeRequestBuilds.forEach((build) => {
                    if (build.id === item.build_result_id) {
                      author = build.trigger_user;
                    }
                  });
                  item.comments.forEach((comment) => {
                    if (!comment.author) {
                      comment.author = {
                        avatar_url: comment.author_avatar_url,
                        name: comment.author_name,
                        id: comment.author_id,
                      };
                    }
                    return comment;
                  });
                  const curDrafts = drafts.filter((draft) => {
                    return draft.issue_id === item.id;
                  });
                  return (
                    <div>
                      <RuleCard
                        rule={item}
                        author={author}
                        headerRightArea={<P3cDetailDialog rule={item} {...this.props} />}
                        commentEle={
                          <Conversation
                            {...{
                              conversation: [...item.comments, ...curDrafts],
                              line,
                              side: change.type === 'delete' ? 'left' : 'right',
                              newpath: newPath,
                              readOnly: item.comments[0] && item.comments[0].is_draft,
                              issueId: item.id,
                              isYundouComment: true,
                              ...this.props,
                            }}
                          />
                        }
                      />
                    </div>
                  );
                })}
              </div>
            ) : null}
            {isCommentBlockShow ? (
              <div className="inline-comment-container">
                {conversations.map((conversation) => (
                  <Conversation
                    conversation={conversation}
                    line={line}
                    side={change.type === 'delete' ? 'left' : 'right'}
                    newpath={newPath}
                    readOnly={conversation[0] && conversation[0].is_draft}
                    {...this.props}
                  />
                ))}

                {writingChanges &&
                writingChanges.includes(
                  DiffWithComment.getLineId(change, oldPath, newPath, diff),
                ) ? (
                  <Conversation
                    conversation={[]}
                    line={line}
                    newpath={newPath}
                    side={change.type === 'delete' ? 'left' : 'right'}
                    onCancel={() => {
                      const lineId = DiffWithComment.getLineId(change, oldPath, newPath, diff);
                      this.props.removeWritingChangesAndEditor(lineId);
                    }}
                    {...this.props}
                  />
                ) : null}
              </div>
            ) : null}
            {matched_inline_issues.length ? (
              <div
                className="inline-rule-card-container"
                id={`violation-${newPath}-${change.newLineNumber}`}
              >
                {matched_inline_issues.map((issue) => {
                  const upvoteFeebacks = issue.feedbacks
                    ? issue.feedbacks.filter((item) => item.feedback === 'upvote')
                    : [];
                  const downvoteFeedbacks = issue.feedbacks
                    ? issue.feedbacks.filter((item) => item.feedback === 'downvote')
                    : [];
                  const hasUpvoted = upvoteFeebacks.find(
                    (item) => item.user_info.extern_uid === window.FORCE.user.staffId,
                  );

                  const hasDownvoted = downvoteFeedbacks.find(
                    (item) => item.user_info.extern_uid === window.FORCE.user.staffId,
                  );

                  return (
                    <IssueCard
                      issue={issue}
                      headerRightArea={
                        <div style={{ display: 'flex', alignItems: 'center' }}>
                          {issue.enable_upvote && (
                            <Balloon
                              align="t"
                              trigger={
                                <span
                                  onClick={() => {
                                    if (!hasUpvoted) {
                                      this.props.postInlineIssuesFeedback({
                                        target_type: 'inline_issue',
                                        target_id: issue.id,
                                        feedback: 'upvote',
                                      });
                                    } else {
                                      this.props.deleteInlineIssuesFeedback({
                                        target_type: 'inline_issue',
                                        target_id: issue.id,
                                        feedback: 'upvote',
                                      });
                                    }
                                  }}
                                  className={`inline-issues-feedback-btn ${
                                    hasUpvoted ? 'active' : ''
                                  }`}
                                >
                                  <YunxiaoIcon type="good" />
                                  {upvoteFeebacks.length}
                                </span>
                              }
                              closable={false}
                            >
                              {upvoteFeebacks.length ? (
                                <div>
                                  {upvoteFeebacks.map((item) => (
                                    <a>{item.user_info.name || item.user_info.username} </a>
                                  ))}
                                  {intl.get({
                                    id: 'code-assets.components.diff.diffWithComment.FeelUseful',
                                    defaultMessage: '觉得有用',
                                  })}
                                </div>
                              ) : (
                                <div>
                                  {intl.get({
                                    id: 'code-assets.components.diff.diffWithComment.Useful',
                                    defaultMessage: '有用',
                                  })}
                                </div>
                              )}
                            </Balloon>
                          )}

                          {issue.enable_downvote && (
                            <Balloon
                              align="t"
                              trigger={
                                <span
                                  onClick={() => {
                                    if (!hasDownvoted) {
                                      this.props.postInlineIssuesFeedback({
                                        target_type: 'inline_issue',
                                        target_id: issue.id,
                                        feedback: 'downvote',
                                      });
                                    } else {
                                      this.props.deleteInlineIssuesFeedback({
                                        target_type: 'inline_issue',
                                        target_id: issue.id,
                                        feedback: 'downvote',
                                      });
                                    }
                                  }}
                                  className={`inline-issues-feedback-btn ${
                                    hasDownvoted ? 'active' : ''
                                  }`}
                                  style={{ marginRight: '4px' }}
                                >
                                  <YunxiaoIcon type="bad" />
                                  {downvoteFeedbacks.length}
                                </span>
                              }
                              closable={false}
                            >
                              {downvoteFeedbacks.length ? (
                                <div>
                                  {downvoteFeedbacks.map((item) => (
                                    <a>{item.user_info.name || item.user_info.username} </a>
                                  ))}
                                  {intl.get({
                                    id: 'code-assets.components.diff.diffWithComment.FeelUseless',
                                    defaultMessage: '觉得无用',
                                  })}
                                </div>
                              ) : (
                                <div>
                                  {intl.get({
                                    id: 'code-assets.components.diff.diffWithComment.Useless',
                                    defaultMessage: '无用',
                                  })}
                                </div>
                              )}
                            </Balloon>
                          )}

                          <InlineIssueDetailDialog issue={issue} {...this.props} />
                        </div>
                      }
                      {...this.props}
                    />
                  );
                })}
              </div>
            ) : null}
          </div>
        ),
      };
    }, {});
  };

  addComment = (change, hunks, oldPath, newPath) => {
    const {
      writingChanges,
      writingChangesEditors,
      commentsVisibleStatus,
      diff,
      changesReducer,
      mrCommentConfig,
    } = this.props;
    const { allComments = [] } = changesReducer;
    const hasPermission =
      pathResource.isArchive === 'false' &&
      utils.allowComments(allComments.length, mrCommentConfig);

    // 无权限或者无评论模式不能添加
    if (!hasPermission || commentsVisibleStatus === 'none') {
      return null;
    }

    const lineId = DiffWithComment.getLineId(change, oldPath, newPath, diff);
    // 如果未打开编辑器则打开，否则选中已打开的编辑器
    if (!writingChanges.includes(lineId)) {
      this.props.addWritingChanges(lineId);
    } else {
      window.CKEDITOR.instances[writingChangesEditors[lineId].name].focus();
    }
  };

  manualLoadDiff = (diff) => {
    this.props.getSingleDiff(diff);
  };

  renderHunk = (hunk) => {
    let hunkHeader = null;
    const { positionMap, diff, codeReviewDetail } = this.props;
    if (hunk.content !== 'STUB') {
      const parsedDiff = parseDiff(diff.diff, { stubHunk: true })[0];
      const oldHunkKey = hunk.content;
      const indexOfHunk = parsedDiff.hunks
        ? parsedDiff.hunks.findIndex((item) => item.content === oldHunkKey)
        : -1;
      const prevHunk = parsedDiff.hunks[indexOfHunk - 1];
      const prevHunkEnd = prevHunk ? prevHunk.newStart + prevHunk.newLines : 0;
      const prevHunkOldEnd = prevHunk ? prevHunk.oldStart + prevHunk.oldLines : 0;

      let from;
      let to;
      let oldFrom;
      let oldTo;

      const loadMoreLinesBtnGutter = (
        <div
          className="load-more-lines-btn-gutter"
          onClick={() => {
            from = hunk.newStart - 21 > 0 ? hunk.newStart - 21 : 1;
            to = hunk.newStart - 1;
            oldFrom = hunk.oldStart - 21 > 0 ? hunk.oldStart - 21 : 1;
            oldTo = hunk.oldStart - 1;
            if (indexOfHunk > 0 && from < prevHunkEnd) {
              from = prevHunkEnd;
            }
            if (indexOfHunk > 0 && oldFrom < prevHunkOldEnd) {
              oldFrom = prevHunkOldEnd;
            }
            this.props.loadMoreLines({
              from,
              to,
              hunk,
              diff: parsedDiff,
              oldScopes: [{ lower_bound: oldFrom, upper_bound: oldTo }],
              newScopes: [{ lower_bound: from, upper_bound: to }],
            });
          }}
        >
          <YunxiaoIcon type="more-line" />
        </div>
      );

      const loadMoreLinesBtns = (
        <div className="load-more-lines-btns">
          <a
            className="load-more-lines-btn"
            onClick={() => {
              track({
                action: 'show_20_lines',
                page: 'Repo-MR-changes',
                mr_id: codeReviewDetail.id,
              });

              from = hunk.newStart - 21 > 0 ? hunk.newStart - 21 : 1;
              to = hunk.newStart - 1;
              oldFrom = hunk.oldStart - 21 > 0 ? hunk.oldStart - 21 : 1;
              oldTo = hunk.oldStart - 1;
              if (indexOfHunk > 0 && from < prevHunkEnd) {
                from = prevHunkEnd;
              }
              if (indexOfHunk > 0 && oldFrom < prevHunkOldEnd) {
                oldFrom = prevHunkOldEnd;
              }
              this.props.loadMoreLines({
                from,
                to,
                hunk,
                diff: parsedDiff,
                oldScopes: [{ lower_bound: oldFrom, upper_bound: oldTo }],
                newScopes: [{ lower_bound: from, upper_bound: to }],
              });
            }}
          >
            <span className="load-more-lines-btn-icon">▲</span>
            {intl.get({
              id: 'code-assets.components.diff.diffWithComment.ExpandRows',
              defaultMessage: '展开20行',
            })}
          </a>
          <a
            className="load-more-lines-btn"
            onClick={() => {
              track({
                action: 'show_all_lines',
                page: 'Repo-MR-changes',
                mr_id: codeReviewDetail.id,
              });

              from = prevHunkEnd || 1;
              to = hunk.newStart - 1;
              oldFrom = prevHunkOldEnd || 1;
              oldTo = hunk.oldStart - 1;
              this.props.loadMoreLines({
                from,
                to,
                hunk,
                diff: parsedDiff,
                oldScopes: [{ lower_bound: oldFrom, upper_bound: oldTo }],
                newScopes: [{ lower_bound: from, upper_bound: to }],
              });
            }}
          >
            {intl.get({
              id: 'code-assets.components.diff.diffWithComment.ExpandAllRows',
              defaultMessage: '展开所有行',
            })}
          </a>
          {indexOfHunk > 0 ? (
            <a
              className="load-more-lines-btn"
              onClick={() => {
                from = prevHunkEnd || 1;
                to = from + 20;
                oldFrom = prevHunkOldEnd || 1;
                oldTo = oldFrom + 20;
                if (to > hunk.newStart - 1) {
                  to = hunk.newStart - 1;
                }
                if (oldTo > hunk.oldStart - 1) {
                  oldTo = hunk.oldStart - 1;
                }
                this.props.loadMoreLines({
                  from,
                  to,
                  hunk,
                  // hunk: prevHunk,
                  diff: parsedDiff,
                  isAddToEnd: true,
                  oldScopes: [{ lower_bound: oldFrom, upper_bound: oldTo }],
                  newScopes: [{ lower_bound: from, upper_bound: to }],
                });
              }}
            >
              <span className="load-more-lines-btn-icon">▼</span>
              {intl.get({
                id: 'code-assets.components.diff.diffWithComment.ExpandRows',
                defaultMessage: '展开20行',
              })}
            </a>
          ) : null}
        </div>
      );

      if (
        hunk.newStart !== 1 &&
        hunk.newStart > prevHunkEnd &&
        !diff.deleted_file &&
        !diff.new_file
      ) {
        hunkHeader = [loadMoreLinesBtnGutter, loadMoreLinesBtns];
      } else {
        hunkHeader = null;
      }
    } else {
      // 如果尾部块的末行小于文件总行数，则尾部块可点击加载更多
      const parsedDiff = parseDiff(diff.diff, { stubHunk: true })[0];
      const oldHunkKey = hunk.content;
      const indexOfHunk = parsedDiff.hunks
        ? parsedDiff.hunks.findIndex((item) => item.content === oldHunkKey)
        : -1;
      const prevHunk = parsedDiff.hunks[indexOfHunk - 1];
      const prevHunkEnd = prevHunk ? prevHunk.newStart + prevHunk.newLines : 0;
      const prevHunkOldEnd = prevHunk ? prevHunk.oldStart + prevHunk.oldLines : 0;

      let from;
      let to;
      let oldFrom;
      let oldTo;

      const loadMoreLinesBtnGutter = (
        <div
          className="load-more-lines-btn-gutter"
          onClick={() => {
            from = diff.lastLineNum || hunk.newStart;
            to = (diff.lastLineNum || hunk.newStart) + 20;
            if (indexOfHunk > 0 && from < prevHunkEnd) {
              from = prevHunkEnd;
            }
            oldFrom = diff.lastLineNum || hunk.oldStart;
            oldTo = (diff.lastLineNum || hunk.oldStart) + 20;
            if (indexOfHunk > 0 && oldFrom < prevHunkOldEnd) {
              oldFrom = prevHunkOldEnd;
            }

            this.props.loadMoreLines({
              from,
              to,
              hunk,
              diff: parsedDiff,
              oldScopes: [{ lower_bound: oldFrom, upper_bound: oldTo }],
              newScopes: [{ lower_bound: from, upper_bound: to }],
            });
          }}
        >
          <YunxiaoIcon type="more-line" />
        </div>
      );

      const loadMoreLinesBtns = (
        <div className="load-more-lines-btns">
          <a
            className="load-more-lines-btn"
            onClick={() => {
              track({
                action: 'show_all_lines',
                page: 'Repo-MR-changes',
                mr_id: codeReviewDetail.id,
              });

              from = diff.lastLineNum || hunk.newStart;
              to = null; // to不传时，为获取到文件最后一行
              oldFrom = diff.lastLineNum || hunk.oldStart;
              oldTo = null;
              this.props.loadMoreLines({
                from,
                to,
                hunk,
                diff: parsedDiff,
                isLoadEndFinished: true,
                oldScopes: [{ lower_bound: oldFrom, upper_bound: oldTo || '' }],
                newScopes: [{ lower_bound: from, upper_bound: to || '' }],
              });
            }}
          >
            {intl.get({
              id: 'code-assets.components.diff.diffWithComment.ExpandAllRows',
              defaultMessage: '展开所有行',
            })}
          </a>
          {indexOfHunk > 0 ? (
            <a
              className="load-more-lines-btn"
              onClick={() => {
                track({
                  action: 'show_20_lines',
                  page: 'Repo-MR-changes',
                  mr_id: codeReviewDetail.id,
                });

                from = diff.lastLineNum || hunk.newStart;
                to = (diff.lastLineNum || hunk.newStart) + 20;
                if (indexOfHunk > 0 && from < prevHunkEnd) {
                  from = prevHunkEnd;
                }
                oldFrom = diff.lastLineNum || hunk.oldStart;
                oldTo = oldFrom + 20;
                if (indexOfHunk > 0 && oldFrom < prevHunkOldEnd) {
                  oldFrom = prevHunkOldEnd;
                }
                this.props.loadMoreLines({
                  from,
                  to,
                  hunk,
                  diff: parsedDiff,
                  oldScopes: [{ lower_bound: oldFrom, upper_bound: oldTo }],
                  newScopes: [{ lower_bound: from, upper_bound: to }],
                });
              }}
            >
              <span className="load-more-lines-btn-icon">▼</span>
              {intl.get({
                id: 'code-assets.components.diff.diffWithComment.ExpandRows',
                defaultMessage: '展开20行',
              })}
            </a>
          ) : null}
        </div>
      );

      // diff.loadEndFinished 标识文件尾部已加载完，此时不用再展示加载更多按钮
      // 且删除的文件也不用展示加载更多按钮
      if (!diff.loadEndFinished && !diff.deleted_file && !diff.new_file) {
        hunkHeader = [loadMoreLinesBtnGutter, loadMoreLinesBtns];
        if (diff.a_mode === '160000') {
          hunkHeader = null;
        }
      } else {
        hunkHeader = null;
      }
    }
    return (
      <Hunk
        key={hunk.content + (diff.new_object_id || '')}
        hunk={hunk}
        header={hunkHeader}
        diff={{ new_path: diff.new_path, old_path: diff.old_path }}
        positionMap={positionMap}
      />
    );
  };

  /**
   * 手动加载更多并跳转指定行
   * @param {*} line 跳转的行号
   * @param {*} side 所处的位置
   * @param {*} cb 回调函数
   */
  getBlobAndGoLine = (line, side, cb) => {
    const { diff } = this.props;
    const parseDiffArr = parseDiff(diff.diff, { stubHunk: true });
    if (!parseDiffArr.length) {
      return;
    }
    let from;
    let to;
    let oldFrom;
    let oldTo;
    let hunk;
    let isAddToEnd;
    const { hunks } = parseDiffArr[0];
    hunks.forEach((item, index) => {
      const prevHunk = hunks[index - 1];
      const prevHunkEnd = prevHunk ? prevHunk.newStart + prevHunk.newLines : 0;
      const prevHunkOldEnd = prevHunk ? prevHunk.oldStart + prevHunk.oldLines : 0;
      const lower_bound = item.newStart;
      const upper_bound = item.newStart + item.newLines - 1;
      const old_lower_bound = item.oldStart;
      const old_upper_bound = item.oldStart + item.oldLines - 1;
      if (prevHunk) {
        const prev_upper_bound = prevHunk.newStart + prevHunk.newLines - 1;
        const prev_old_upper_bound = prevHunk.oldStart + prevHunk.oldLines - 1;
        // 二者之间
        if (
          (side === 'left' && prev_old_upper_bound < line && old_lower_bound > line) ||
          (side === 'right' && prev_upper_bound < line && lower_bound > line)
        ) {
          // 二者之间判断在哪个位置,向下20或者向上20或者全部展开
          const pre_distance = line - (side === 'left' ? prev_old_upper_bound : prev_upper_bound);
          const next_distance = (side === 'left' ? old_lower_bound : lower_bound) - line;
          if (pre_distance <= 20) {
            from = prevHunkEnd || 1;
            to = from + 20;
            oldFrom = prevHunkOldEnd || 1;
            oldTo = oldFrom + 20;
            if (to > item.newStart - 1) {
              to = item.newStart - 1;
            }
            if (oldTo > item.oldStart - 1) {
              oldTo = item.oldStart - 1;
            }
            hunk = item;
            isAddToEnd = true;
          } else if (next_distance <= 20) {
            from = item.newStart - 21 > 0 ? item.newStart - 21 : 1;
            to = item.newStart - 1;
            oldFrom = item.oldStart - 21 > 0 ? item.oldStart - 21 : 1;
            oldTo = item.oldStart - 1;
            if (from < prevHunkEnd) {
              from = prevHunkEnd;
            }
            if (oldFrom < prevHunkOldEnd) {
              oldFrom = prevHunkOldEnd;
            }
            hunk = item;
          } else {
            from = prevHunkEnd || 1;
            to = item.newStart - 1;
            oldFrom = prevHunkOldEnd || 1;
            oldTo = item.oldStart - 1;
            hunk = item;
          }
          this.props.loadMoreLines({
            from,
            to,
            hunk,
            diff: parseDiffArr[0],
            isAddToEnd,
            oldScopes: [{ lower_bound: oldFrom, upper_bound: oldTo || '' }],
            newScopes: [{ lower_bound: from, upper_bound: to || '' }],
          });
        }
        // 最后一个
        if (
          index === hunks.length - 1 &&
          ((side === 'left' && old_upper_bound < line) || (side === 'right' && upper_bound < line))
        ) {
          from = diff.lastLineNum || item.newStart;
          to = null; // to不传时，为获取到文件最后一行
          oldFrom = diff.lastLineNum || item.oldStart;
          oldTo = null;
          hunk = item;
          const isLoadEndFinished = true;
          this.props.loadMoreLines({
            from,
            to,
            hunk,
            diff: parseDiffArr[0],
            oldScopes: [{ lower_bound: oldFrom, upper_bound: oldTo || '' }],
            newScopes: [{ lower_bound: from, upper_bound: to || '' }],
            isLoadEndFinished,
          });
        }
      } else if (
        (side === 'left' && old_lower_bound > line) ||
        (side === 'right' && lower_bound > line)
      ) {
        // 从1开始请求
        from = item.newStart - 21 > 0 ? item.newStart - 21 : 1;
        to = item.newStart - 1;
        oldFrom = item.oldStart - 21 > 0 ? item.oldStart - 21 : 1;
        oldTo = item.oldStart - 1;
        if (from > line) {
          from = 1;
        }
        if (oldFrom > line) {
          oldFrom = 1;
        }
        hunk = item;
        this.props.loadMoreLines({
          from,
          to,
          hunk,
          diff: parseDiffArr[0],
          oldScopes: [{ lower_bound: oldFrom, upper_bound: oldTo || '' }],
          newScopes: [{ lower_bound: from, upper_bound: to || '' }],
        });
      }
    });
    cb && cb();
  };

  renderImgJsx = (commitId, path, type) => {
    return (
      <div className={`diff-single-img ${type}`}>
        <img
          src={getPathWithRootPath(`/${pathResource.fullPath}/raw/${utils.revisionEncodeURI(commitId)}/${path}`)}
          style={{ maxWidth: '90%' }}
        />
      </div>
    );
  };

  renderLfsFile = () => {
    return (
      <div className="manual-load-diff-holder" style={{ cursor: 'initial' }}>
        {intl.get({
          id: 'code-assets.components.diff.diffWithComment.GitLfsFileComparisonIs',
          defaultMessage: '暂不支持Git LFS文件比较',
        })}
      </div>
    );
  };

  render() {
    const {
      diff,
      viewType,
      commentsVisibleStatus,
      commits,
      from_commit,
      to_commit,
      codeReviewDetail,
    } = this.props;
    if (diff.isLoading) {
      return (
        <div className="single-diff-loading">
          <Loading style={{ height: '100px' }} />
        </div>
      );
    }

    // 因为现在改成了单文件渲染，原有$WAITING_FOR_CLICK$标识的直接渲染，$WAITING_FOR_CLICK_BIG_SIZE$标识的才需用户二次点击
    if (diff.diff === '$WAITING_FOR_CLICK$' && !diff.isLoading) {
      this.props.getSingleDiff(diff, this.props.hash.line);
      return (
        <div className="manual-load-diff-holder" style={{ cursor: 'initial' }}>
          <ListLoading />
        </div>
      );
    }
    // $WAITING_FOR_CLICK_BIG_SIZE$标识了文件体积过大，可能卡死页面
    if (diff.diff === '$WAITING_FOR_CLICK_BIG_SIZE$') {
      return (
        <div className="manual-load-diff-holder">
          <a onClick={this.manualLoadDiff.bind(null, diff)}>
            {intl.get({
              id: 'code-assets.components.diff.diffWithComment.ThereAreTooManyChanges',
              defaultMessage:
                '此文件或本次改动内容过多，您可以点此手动加载本文件的改动（可能引起页面卡顿，请耐心等待）',
            })}
          </a>
        </div>
      );
    }

    const is_lfs = diff.is_new_lfs || diff.is_old_lfs;
    if (is_lfs && diff.showDiff) {
      return this.renderLfsFile();
    }

    // 如果 showDiff: false 表示二进制等无法展示的文件，返回提示文案
    if (!diff.showDiff) {
      if (
        diff.is_binary &&
        utils.isImageSuffix(diff.deleted_file ? diff.old_path : diff.new_path)
      ) {
        let oldCommitId = '';
        const oldCommitIndex = commits.findIndex((item) => item.id === from_commit);
        oldCommitId =
          oldCommitIndex > 0
            ? commits[oldCommitIndex - 1].id
            : codeReviewDetail.target_branch || codeReviewDetail.targetBranch;

        if (diff.deleted_file) {
          return this.renderImgJsx(oldCommitId, diff.old_path, 'delete');
        }
        if (diff.new_file) {
          return this.renderImgJsx(to_commit, diff.new_path, 'new');
        }
        return (
          <div className="diff-img-wraper">
            {this.renderImgJsx(oldCommitId, diff.old_path, 'delete')}
            {this.renderImgJsx(to_commit, diff.new_path, 'new')}
          </div>
        );
      }

      if (is_lfs) {
        return this.renderLfsFile();
      }
      return (
        <div className="manual-load-diff-holder" style={{ cursor: 'initial' }}>
          {intl.get({
            id: 'code-assets.components.diff.diffWithComment.CurrentlyYouCannotViewChanges',
            defaultMessage: '暂不支持查看二进制文件改动',
          })}
        </div>
      );
    }

    const parseDiffArr = parseDiff(diff.diff, { stubHunk: true });

    // commentsVisibleStatus 可取3个值：all none unClosed
    // none 为隐藏所有评论
    const isCommentUnVisible = commentsVisibleStatus === 'none';

    let diffType = '';
    if (diff.deleted_file) {
      diffType = 'delete';
    } else if (diff.new_file) {
      diffType = 'add';
    }

    return (
      <div>
        {parseDiffArr.map(({ hunks, oldPath, newPath }, i) =>
          (hunks.length ? (
            <Diff
              key={i}
              viewType={viewType || 'unified'}
              widgets={this.getWidgets(hunks, oldPath, newPath)}
              customEvents={this.getCustomEvents(hunks, oldPath, newPath)}
              markEdits={markWordEdits({ threshold: 100, markLongDistanceDiff: false })}
              generateAnchorID={(change) => {
                const { oldLineNumber, newLineNumber, type } = change;
                const side = type === 'delete' ? 'left' : 'right';
                const line = type === 'delete' ? oldLineNumber : newLineNumber;
                const path = type === 'delete' ? oldPath : newPath;
                return `anchor_line_${sha1(path)}_${side}_${line}`;
                // return `anchor_line_${sha1(newPath)}_${oldLineNumber}_${newLineNumber}`;
              }}
              generateChangeID={(change) => {
                const { oldLineNumber, newLineNumber, type } = change;
                const side = type === 'delete' ? 'left' : 'right';
                const line = type === 'delete' ? oldLineNumber : newLineNumber;
                const path = type === 'delete' ? oldPath : newPath;
                return `td_change_${sha1(path)}_${side}_${line}`;
              }}
              // customClassNames={{ line: 'saboo-line' }}
              onRenderCode={(td) => {
                // 代码高亮逻辑
                const fileName = newPath.split('/')[newPath.split('/').length - 1];
                let language = '';
                if (fileName.indexOf('.') !== -1) {
                  language = fileName.split('.')[fileName.split('.').length - 1];
                }
                const originalClass = td.getAttribute('class');
                // 若无后缀，当做js处理，以展示成全黑（若写空或写txt都会被hljs识别为nginx）
                td.setAttribute(
                  'class',
                  `${originalClass} ${language === 'txt' || !language ? 'js' : language}`,
                );
                hljs.highlightBlock(td);
                return td;
              }}
              diffType={diffType}
              genWatermarkimage={this.props.genWatermarkimage}
            >
              {hunks.map(this.renderHunk)}
            </Diff>
          ) : (
            <div>
              <ListEmpty
                size="small"
                type="card"
                content=""
                description={intl.get({
                  id: 'code-assets.components.diff.diffWithComment.NoData',
                  defaultMessage: '暂无数据',
                })}
                image="none"
                style={{ background: '#fff', paddingBottom: 20 }}
              />
            </div>
          )))}
        {this.state.restConversationsOfThisDiff.length && !isCommentUnVisible ? (
          <div className="inline-comment-container notMatched-comment-container">
            {this.state.restConversationsOfThisDiff.map((conversation) => (
              <Conversation conversation={conversation} notMatched {...this.props} />
            ))}
          </div>
        ) : null}
      </div>
    );
  }
}
